package com.guangzhoujiayou.mongo;

import com.mongodb.ConnectionString;
import com.mongodb.MongoCredential;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 */
//@Configuration  有这个注解，就不用在启动类上加@EnableHongMongo 。但是要注意包要被扫描
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoClientOptionProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class JiaYouMongoDBConfig {


    /**
     * 此Bean也是可以不显示定义的，如果我们没有显示定义生成MongoTemplate实例，
     * SpringBoot利用我们配置好的MongoDbFactory在配置类中生成一个MongoTemplate，
     * 之后我们就可以在项目代码中直接@Autowired了。因为用于生成MongoTemplate
     * 的MongoDbFactory是我们自己在MongoConfig配置类中生成的，所以我们自定义的连接池参数也就生效了。
     *
     * @param simpleMongoClientDatabaseFactory mongo工厂
     * @param converter      转换器
     * @return MongoTemplate实例
     */
    @Bean
    public MongoTemplate mongoTemplate(SimpleMongoClientDatabaseFactory simpleMongoClientDatabaseFactory, MappingMongoConverter converter) {
        MongoTemplate mongoTemplate = new MongoTemplate(simpleMongoClientDatabaseFactory, converter);
        // 设置读从库优先
        mongoTemplate.setReadPreference(ReadPreference.secondaryPreferred());
        return mongoTemplate;
    }

    /**
     * 转换器
     * MappingMongoConverter可以自定义mongo转换器，主要自定义存取mongo数据时的一些操作，例如 mappingConverter.setTypeMapper(new
     * DefaultMongoTypeMapper(null)) 方法会将mongo数据中的_class字段去掉。
     *
     * @param simpleMongoClientDatabaseFactory     mongo工厂
     * @param context     上下文
     * @param beanFactory 获取自定义转换器 CustomConversions
     * @return 转换器对象
     */
    @Bean
    public MappingMongoConverter mappingMongoConverter(SimpleMongoClientDatabaseFactory simpleMongoClientDatabaseFactory,
                                                       MongoMappingContext context,
                                                       BeanFactory beanFactory) {
        // 创建 DbRefResolver 对象
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(simpleMongoClientDatabaseFactory);
        // 创建 MappingMongoConverter 对象
        MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
        // 设置 conversions 属性
        try {
            mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
        } catch (NoSuchBeanDefinitionException ignore) {
        }
        // 设置 typeMapper 属性，从而移除 _class field 。
        mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return mappingConverter;
    }

    /**
     * 自定义mongo连接池
     *
     * @param properties 属性配置类
     * @return MongoDbFactory对象
     */
    @Bean
    public SimpleMongoClientDatabaseFactory mongoDbFactory(MongoClientOptionProperties properties ) {
        String url = connectionString(properties);
        return new SimpleMongoClientDatabaseFactory(url);
    }

    /**
     * 创建认证
     *
     * @param properties 属性配置类
     * @return 认证对象
     */
    private MongoCredential getCredential(MongoClientOptionProperties properties) {
        if (!StringUtils.isEmpty(properties.getUsername()) && !StringUtils.isEmpty(properties.getPassword())) {
            // 没有专用认证数据库则取当前数据库
            String database = StringUtils.isEmpty(properties.getAuthenticationDatabase()) ?
                    properties.getDatabase() : properties.getAuthenticationDatabase();
            return MongoCredential.createCredential(properties.getUsername(), database,
                    properties.getPassword().toCharArray());
        }
        return null;
    }

    /**
     * 获取数据库服务地址
     *
     * @param mongoAddress 地址字符串
     * @return 服务地址数组
     */
    private List<ServerAddress> getServerAddress(String mongoAddress) {
        String[] mongoAddressArray = mongoAddress.trim().split(",");
        List<ServerAddress> serverAddressList = new ArrayList<>(4);
        for (String address : mongoAddressArray) {
            String[] hostAndPort = address.split(":");
            serverAddressList.add(new ServerAddress(hostAndPort[0], Integer.parseInt(hostAndPort[1])));
        }
        return serverAddressList;
    }

    /**
     * mongo客户端参数配置
     *
     * @param properties 属性配置类
     * @return mongo客户端参数配置对象
     */
    private String connectionString(MongoClientOptionProperties properties) {
//mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
        String url = "mongodb://"+properties.getUsername()+":"+properties.getPassword()
                +"@"+properties.getAddress()+"/"+properties.getDatabase()
                ;
        /**
         @see ConnectionString
         minConnectionPoolSize   最小连接池大小
         maxConnectionPoolSize  最大连接池大小
         maxWaitTime    最大等待时间
         maxConnectionIdleTime  最大连接空闲时间
         maxConnectionLifeTime  最大连接寿命
         connectTimeout 连接超时
         socketTimeout  套接字超时
         sslEnabled 启用ssl
         sslInvalidHostnameAllowed  允许ssl无效主机名
         requiredReplicaSetName 必需的副本集名称
         serverSelectionTimeout 服务器选择超时
         localThreshold 本地阈值
         heartbeatFrequency 心跳频率
         applicationName    应用程序名称
          compressorList    压缩机
          uuidRepresentation    uuid表示
         */
        //封装 options  "&|;" 分割
        Map<String, String> map = new HashMap<>();
        map.put("minConnectionPoolSize", String.valueOf(properties.getMinConnectionsPerHost()));
        map.put("maxConnectionPoolSize", String.valueOf(properties.getConnectionsPerHost()));
        map.put("maxWaitTime", String.valueOf(properties.getMaxWaitTimeMs()));
        map.put("maxConnectionIdleTime", String.valueOf(properties.getMaxConnectionIdleTimeMs()));
        map.put("maxConnectionLifeTime", String.valueOf(properties.getMaxConnectionLifeTimeMs()));
        map.put("connectTimeout", String.valueOf(properties.getConnectionTimeoutMs()));
        map.put("socketTimeout", String.valueOf(properties.getReadTimeoutMs()));
        map.put("applicationName", String.valueOf(properties.getClientName()));
        map.put("heartbeatFrequency", String.valueOf(properties.getHeartbeatFrequencyMs()));
        List<String> options = new ArrayList<>();
        for (String key : map.keySet()) {
            String value = map.get(key);
            if (!StringUtils.isEmpty(value)) {
                String option = key + "=" + value;
                options.add(option);
            }
        }
        if (!CollectionUtils.isEmpty(options)) {
            url = url + "?" + String.join("&", options);
        }
        return url;
    }


}
